package org.swift.mashup.engine.concurrency;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.swift.mashup.engine.MashupException;

public class ConcurrencySettingsLoader implements ConcurrentConfiguration {
	private static final Logger log = Logger
		.getLogger(ConcurrencySettingsLoader.class);

	private static final String DEFAULT_CONTEXT_TYPE = "local";
	private static final String SETTINGS_PATH = "/settings";
	private static final String CONCURRENT_PROP_FILE = "concurrent.properties";
	private static final String COMPUTE_NODES_FILE = "computeNodes.cfg";
	private static final String CONCURRENT_CONTEXT_TYPE = "org.swift.concurrent.ContextType";
	private static final String CONCURRENT_JOB_MAX_ATTEMPTS = "org.swift.concurrent.MaxAttempts";
	private static final String CONCURRENT_DELETE_FILES = "org.swift.concurrent.DeleteFiles";
	private static final String CONCURRENT_MAPPED_FILES = "org.swift.concurrent.MappedFiles";
	
	/**
	 * When the settings are loaded, the last modified time for any accessed
	 * file is put into this map. That way, when settings are reloaded, we can
	 * check for each file to see if the lastmodifiedtime is different before
	 * reloading it.
	 */
	private final Map<File, Long> lastModifiedTimes = new HashMap<File, Long>();
	private final File settingsDir;
	private final String workingDir;
		
	private boolean autoRefresh = true;	
	private Collection<String> computeNodes;
	private Properties concurrentProperties;

	/**
	 * 
	 * @param settingsDirectory
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	public ConcurrencySettingsLoader(String workingDirectory)
			throws MashupException {
		this.workingDir = workingDirectory;
		this.settingsDir = new File(workingDirectory,SETTINGS_PATH);
		loadSettings();
	}

	/**
	 * @param settingsDirectory
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	public ConcurrencySettingsLoader(File workingDirectory)
			throws MashupException {
		this.workingDir = workingDirectory.getPath();
		this.settingsDir = new File(workingDirectory,SETTINGS_PATH);
		loadSettings();
	}

	/**
	 * 
	 * @param settingsDirectory
	 * @param autoRefresh
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	public ConcurrencySettingsLoader(File workingDirectory, boolean autoRefresh)
			throws MashupException {
		this.workingDir = workingDirectory.getAbsolutePath();
		this.settingsDir = new File(workingDirectory,SETTINGS_PATH);
		this.autoRefresh = autoRefresh;
		loadSettings();
	}

	/**
	 * 
	 * @param workingDirectory
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	public ConcurrencySettingsLoader(String workingDirectory,
			boolean autoRefresh) throws MashupException {
		this.workingDir = workingDirectory;
		this.settingsDir = new File(workingDirectory, SETTINGS_PATH);
		this.autoRefresh = autoRefresh;
		loadSettings();
	}
	
	/**
	 * 
	 * @param workingDirectory
	 * @param autoRefresh
	 * @param settingsDirectory
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	public ConcurrencySettingsLoader(String workingDirectory,
			boolean autoRefresh, File settingsDirectory) throws MashupException {
		this.workingDir = workingDirectory;
		this.settingsDir = settingsDirectory;
		this.autoRefresh = autoRefresh;
		loadSettings();
	}

	/**
	 * Loads the settings located in the settings directory of the current
	 * working directory.
	 * 
	 * @throws MashupException if, for any reason, the concurrent settings fail
	 * get loaded successfully.
	 */
	protected void loadSettings() throws MashupException {
		setupSettings();
		try {
			log.debug("Loading concurrency settings for the mashup engine.");
			File concProps = new File(settingsDir, CONCURRENT_PROP_FILE);
			lastModifiedTimes.put(concProps, concProps.lastModified());
			loadConcurrentProperties(concProps);
			File computeNodeCfg = new File(settingsDir, COMPUTE_NODES_FILE);;
			lastModifiedTimes.put(computeNodeCfg, computeNodeCfg.lastModified());
			loadComputeNodes(computeNodeCfg);
			log.debug("Settings were loaded succesfully.  Number of compute " +
				"nodes found = " + computeNodes.size());
		} catch (FileNotFoundException fnfe) {
			throw new MashupException(
				"The system failed to construct default settings.  Please verify " +
				"that this application has permission to write files to the " +
				"working directory " + workingDir,fnfe);
		} catch (IOException ioe) {
			throw new MashupException("Unable to load concurrency settings.", ioe);
		}
	}
	
	/**
	 * Creates default directories and files if they do not exist already
	 */
	protected void setupSettings () throws MashupException {
		if (!settingsDir.exists()){
			settingsDir.mkdirs();
		}
		File concurrentPropFile = new File(settingsDir, CONCURRENT_PROP_FILE);
		if (!concurrentPropFile.exists()){
			try {
				concurrentPropFile.createNewFile();
			} catch (IOException ioe){
				throw new MashupException("Failed to create the default concurrent properties file.",ioe);
			}
		}
		File computeNodes = new File(settingsDir, COMPUTE_NODES_FILE);
		if (!computeNodes.exists()){
			try {
				computeNodes.createNewFile();
			} catch (IOException ioe){
				throw new MashupException("Failed to create the default compute nodes file.",ioe);
			}
		}
	}
	
	protected void loadComputeNodes (File fromFile) throws IOException {
		computeNodes = new ArrayList<String>();
		BufferedReader reader = new BufferedReader (new FileReader(fromFile));
		String computeNode = null;
		while ((computeNode = reader.readLine()) != null){
			if (!StringUtils.isEmpty(computeNode)){
				computeNodes.add(computeNode);
			}
		}
		reader.close();
	}

	private void loadConcurrentProperties(File fromFile) throws FileNotFoundException,
			IOException, MashupException {
		concurrentProperties = new Properties();
		InputStream is = new FileInputStream(fromFile);
		concurrentProperties.load(is);
		is.close();
		//System.getProperties().putAll(concurrentProperties);
	}

	/**
	 * Returns true if the file has been modified since the last time we
	 * accessed the information in the file.
	 * 
	 * This method depends on the file's lastModified time being present in the
	 * lastModifiedTimes map.
	 * 
	 * @param file
	 * @return
	 */
	protected boolean hasNewChanges(File file) {
		// Get the last modified time which was pulled off the file whenever we
		// processed it
		Long lastModified = lastModifiedTimes.get(file);
		if (lastModified != null) {
			return file.lastModified() > lastModified;
		}
		return true;
	}

	/**
	 * Checks the last modified date on the settings folder
	 * 
	 * @param cacheKey searches in the cache for the given key. if it is not
	 * located in the cache, then the settings are reloaded.
	 * @return
	 */
	protected void reloadSettingsIfNecessary() {
		if (autoRefresh) {
			try {
				for (File file : lastModifiedTimes.keySet()) {
					if (hasNewChanges(file)) {
						log.debug("Change detected in file " + file.getAbsolutePath() +".  Reloading settings.");
						loadSettings();
						return;
					}
				}
				log.debug("No changes were detected.  Not necessary to reload settings.");
			} catch (MashupException me) {
				throw new RuntimeException("An error occurred trying to "
					+ "load the settings.", me);
			}
		}
	}

	protected Collection<String> getComputeNodes() {
		return computeNodes;
	}

	public int computeNodeCount() {
		reloadSettingsIfNecessary();
		return computeNodes.size();
	}

	public Iterator<String> getComputeNodeIterator() {
		return getComputeNodes().iterator();
	}

	public boolean isAutoRefreshEnabled() {
		return autoRefresh;
	}
	
	public String getWorkingDirectory() {
		return workingDir;
	}
	
	public String getSettingsDirectory() {
		return settingsDir.getAbsolutePath();
	}

	public String getContextType() {
		String ctxType = concurrentProperties.getProperty(CONCURRENT_CONTEXT_TYPE);
		if (StringUtils.isEmpty(ctxType)){
			ctxType = DEFAULT_CONTEXT_TYPE;
		}
		return ctxType.toLowerCase();
	}
	
	public int getMaxAttempts (){
		String maxAttempts = concurrentProperties.getProperty(CONCURRENT_JOB_MAX_ATTEMPTS);
		if (maxAttempts != null){
			return Integer.parseInt(maxAttempts);
		}
		return 1;
	}
	public boolean isDeleteFiles(){
		String shouldDelete = this.concurrentProperties.getProperty(CONCURRENT_DELETE_FILES);
		boolean delete = (shouldDelete==null)?true:Boolean.parseBoolean(shouldDelete);
		return delete;
	}
	public boolean isMappedFiles(){
		String shouldMap = this.concurrentProperties.getProperty(CONCURRENT_MAPPED_FILES);
		boolean mapped = (shouldMap==null)?true:Boolean.parseBoolean(shouldMap);
		return mapped;
	}
}
